using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.IO;
using System.Reflection;
using System.Text;
using System.Xml;

namespace ErrorDumper.Dumping {
    public static class DumpSerializer {
        const string _dateTimeFormat = "s";
        const int _defaultMaxLevelCount = 50;

        #region Serialize

        public static string Serialize(object obj, string name) {
            if (name == null)
                throw new ArgumentNullException("name");
            if (name.Length == 0)
                throw new ArgumentException("is empty", "name");

            using (StringWriter sw = new StringWriter())
            using (XmlTextWriter xw = new XmlTextWriter(sw)) {
                xw.WriteStartElement(name);
                if (obj != null)
                    Serialize(obj, xw);
                xw.WriteEndElement();
                xw.Flush();
                return sw.ToString();
            }
        }

        public static void Serialize(object obj, XmlWriter writer) {
            IDictionary _registry = new HybridDictionary();
            Serialize(obj, writer, _registry, _defaultMaxLevelCount);
            _registry.Clear();
            GC.SuppressFinalize(_registry);
        }

        static void WriteFailedProperty(XmlWriter writer, string propName, Exception ex) {
            writer.WriteStartElement(ExceptionProperty.EncodeName(propName));
            writer.WriteAttributeString("__exception",
                ex != null ? ex.GetType().FullName : string.Empty);
            writer.WriteEndElement();
        }

        static void WriteNullProperty(XmlWriter writer, string propName) {
            writer.WriteStartElement(ExceptionProperty.EncodeName(propName));
            writer.WriteAttributeString("__isNull", string.Empty);
            writer.WriteEndElement();
        }

        public static void Serialize(object obj, XmlWriter writer, IDictionary registry) {
            Serialize(obj, writer, registry, _defaultMaxLevelCount);
        }

        static string FormatDictionaryKey(object key) {
            if (key == null)
                return "null-key";

            try {
                return ExceptionProperty.EncodeName(key.ToString());
            }
            catch {
                return "failed-key";
            }
        }

        public static void Serialize(object obj, XmlWriter writer, IDictionary registry, int maxLevelCount) {
            if (maxLevelCount <= 0)
                return;

            if (obj == null)
                return;

            Type _type = obj.GetType();

            // BEGIN OF CUSTOM-VALUE-TYPES

            if (_type == typeof(string)) {
                writer.WriteString((string)obj);
                return;
            }

            if (_type == typeof(DateTime)) {
                writer.WriteString(((DateTime)obj).ToString(_dateTimeFormat));
                return;
            }

            if (_type == typeof(DictionaryEntry)) {
                DictionaryEntry _entry = (DictionaryEntry)obj;
                writer.WriteStartElement(FormatDictionaryKey(_entry.Key));
                object __value = _entry.Value;
                if (__value != null)
                    Serialize(__value, writer, registry, maxLevelCount - 1);
                writer.WriteEndElement();
                return;
            }

            if (_type.IsGenericType && _type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>)) {
                object __key = _type.InvokeMember("Key", BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty, null, obj, null);
                object __value = _type.InvokeMember("Value", BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty, null, obj, null);

                writer.WriteStartElement(FormatDictionaryKey(__key));
                if (__value != null)
                    Serialize(__value, writer, registry, maxLevelCount - 1);
                writer.WriteEndElement();
                return;
            }

            // END OF CUSTOM-VALUE-TYPES

            if (_type.IsClass) {
                object __ref = registry[obj];
                if (__ref != null) {
                    writer.WriteAttributeString("__ref", __ref.ToString());
                    return;
                }
                int __id = registry.Count + 1;
                writer.WriteAttributeString("__id", __id.ToString());
                registry.Add(obj, __id);
            }

            // BEGIN OF CLASS-TYPES

            if (_type == typeof(byte[])) {
                byte[] _bytes = (byte[])obj;
                writer.WriteBase64(_bytes, 0, _bytes.Length);
                return;
            }

            if (_type == typeof(char[])) {
                char[] _chars = (char[])obj;
                writer.WriteString(new string(_chars));
                return;
            }

            // [DumpContract]

            var dumpAttr = Attribute.GetCustomAttribute(_type, typeof(DumpContractAttribute)) as DumpContractAttribute;
            if (dumpAttr != null) {
                Type nonDumpAttrType = typeof(IgnoreDumpAttribute);
                Type memberAttrType = typeof(DumpMemberAttribute);

                foreach (var member in _type.GetMembers(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.GetField | BindingFlags.GetProperty | BindingFlags.InvokeMethod)) {
                    if (Attribute.IsDefined(member, nonDumpAttrType))
                        continue;

                    var memberAttr = Attribute.GetCustomAttribute(member, memberAttrType) as DumpMemberAttribute;
                    if (memberAttr == null && (!dumpAttr.DumpMembers || member.MemberType == MemberTypes.Method))
                        continue;

                    var name = member.Name;
                    if (memberAttr != null && !string.IsNullOrEmpty(memberAttr.Name))
                        name = memberAttr.Name;

                    object value = null;
                    try {
                        switch (member.MemberType) {
                            case MemberTypes.Field:
                                value = ((FieldInfo)member).GetValue(obj);
                                break;
                            case MemberTypes.Property:
                                value = ((PropertyInfo)member).GetValue(obj, null);
                                break;
                            case MemberTypes.Method:
                                value = ((MethodInfo)member).Invoke(obj, null);
                                break;
                        }
                    }
                    catch (Exception ex) {
                        WriteFailedProperty(writer, member.Name, ex);
                        continue;
                    }

                    if (value == null) {
                        WriteNullProperty(writer, name);
                        continue;
                    }

                    writer.WriteStartElement(ExceptionProperty.EncodeName(name));
                    Serialize(value, writer, registry, maxLevelCount - 1);
                    writer.WriteEndElement();
                }
                return;
            }

            if (obj is Exception) {
                writer.WriteElementString("ClassFullName", _type.FullName);
                PropertyInfo[] __props = _type.GetProperties(BindingFlags.Instance | BindingFlags.Public);
                if (__props != null && __props.Length > 0) {
                    bool _isConfigExc = _type.FullName == "System.Configuration.ConfigurationErrorsException";
                    foreach (PropertyInfo __prop in __props) {
                        if (_isConfigExc && __prop.Name == "Errors")
                            continue;

                        object _value;
                        try {
                            _value = __prop.GetValue(obj, null); }
                        catch (Exception ex) {
                            WriteFailedProperty(writer, __prop.Name, ex);
                            continue;
                        }

                        if (_value == null) {
                            WriteNullProperty(writer, __prop.Name);
                            continue;
                        }

                        writer.WriteStartElement(ExceptionProperty.EncodeName(__prop.Name));
                        Serialize(_value, writer, registry, maxLevelCount - 1);
                        writer.WriteEndElement();
                    }
                }
                return;
            }

            if (EnumerableHelper.Each(obj, item => {
                if (item != null) {
                    Type _itemType = item.GetType();
                    if (_itemType == typeof(DictionaryEntry)) {
                        Serialize(item, writer, registry, maxLevelCount - 1);
                        return;
                    }
                    if (_itemType.IsGenericType && _itemType.GetGenericTypeDefinition() == typeof(KeyValuePair<,>)) {
                        Serialize(item, writer, registry, maxLevelCount - 1);
                        return;
                    }
                }
                writer.WriteStartElement("__Item");
                if (item != null)
                    Serialize(item, writer, registry, maxLevelCount - 1);
                writer.WriteEndElement();
            }))
                return;

            TypeConverter _converter = TypeDescriptor.GetConverter(obj);
            PropertyDescriptorCollection _props = _converter.GetProperties(obj);
            if (_props != null && _props.Count > 0) {
                foreach (PropertyDescriptor _prop in _props) {
                    object _value;
                    try { 
                        _value = _prop.GetValue(obj); }
                    catch (Exception ex) {
                        WriteFailedProperty(writer, _prop.Name, ex);
                        continue;
                    }

                    if (_value == null) {
                        WriteNullProperty(writer, _prop.Name);
                        continue;
                    }

                    writer.WriteStartElement(ExceptionProperty.EncodeName(_prop.Name));
                    Serialize(_value, writer, registry, maxLevelCount - 1);
                    writer.WriteEndElement();
                }
                return;
            }

            if (_converter.CanConvertTo(typeof(string)))
                try {
                    writer.WriteString((string)_converter.ConvertTo(obj, typeof(string)));
                    return;
                }
                catch { } // because invalid enum could not be converted to string: 

            writer.WriteString(obj.ToString());
        }

        #endregion

        #region SerializeConsole
        public static string SerializeConsole(object obj, string name) {
            if (obj == null)
                return name + ": Null";

            XmlDocument doc = new XmlDocument();
            doc.LoadXml(Serialize(obj, name));
            StringBuilder sb = new StringBuilder(4096);
            _SerializeConsole(doc, sb, 0);
            return sb.ToString().Trim();
        }

        static void _SerializeConsole(XmlNode node, StringBuilder sb, int level) {
            bool hasChilds = false;

            if (node.Attributes != null)
                foreach (XmlAttribute attr in node.Attributes) {
                    sb.AppendLine();
                    sb.Append(string.Empty.PadLeft(level * 2));
                    sb.Append(attr.Name);
                    sb.Append(": ");
                    sb.Append(attr.Value);
                    if (!attr.Name.StartsWith("__"))
                        hasChilds = true;
                }

            if (node.ChildNodes != null)
                foreach (XmlNode child in node.ChildNodes)
                    if (child.NodeType == XmlNodeType.Element) {
                        sb.AppendLine();
                        sb.Append(string.Empty.PadLeft(level * 2));
                        sb.Append(child.Name);
                        _SerializeConsole(child, sb, level + 1);
                        hasChilds = true;
                    }

            if (!hasChilds) {
                sb.Append(": ");
                string _text = node.InnerText.Trim();
                sb.Append(string.IsNullOrEmpty(_text) ? "<Empty>" : _text);
            }
        }
        #endregion
    }
}